diff --git a/web/media/multimedia-modal.react.js b/web/media/multimedia-modal.react.js index 17aec8580..6d4ca720d 100644 --- a/web/media/multimedia-modal.react.js +++ b/web/media/multimedia-modal.react.js @@ -1,71 +1,83 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { XCircle as XCircleIcon } from 'react-feather'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import css from './media.css'; type BaseProps = { + +type: string, +uri: string, }; type Props = { ...BaseProps, +popModal: (modal: ?React.Node) => void, }; class MultimediaModal extends React.PureComponent { overlay: ?HTMLDivElement; componentDidMount() { invariant(this.overlay, 'overlay ref unset'); this.overlay.focus(); } render(): React.Node { + let mediaModalItem; + if (this.props.type === 'photo') { + mediaModalItem = ; + } else { + mediaModalItem = ( + + ); + } + return (
- + {mediaModalItem}
); } overlayRef: (overlay: ?HTMLDivElement) => void = overlay => { this.overlay = overlay; }; onBackgroundClick: (event: SyntheticEvent) => void = event => { if (event.target === this.overlay) { this.props.popModal(); } }; onKeyDown: (event: SyntheticKeyboardEvent) => void = event => { if (event.key === 'Escape') { this.props.popModal(); } }; } function ConnectedMultiMediaModal(props: BaseProps): React.Node { const modalContext = useModalContext(); return ; } export default ConnectedMultiMediaModal; diff --git a/web/media/multimedia.react.js b/web/media/multimedia.react.js index 76e13800b..f81df7b18 100644 --- a/web/media/multimedia.react.js +++ b/web/media/multimedia.react.js @@ -1,154 +1,154 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import { CircularProgressbar } from 'react-circular-progressbar'; import 'react-circular-progressbar/dist/styles.css'; import { XCircle as XCircleIcon, AlertCircle as AlertCircleIcon, } from 'react-feather'; import { useModalContext, type PushModal, } from 'lib/components/modal-provider.react.js'; import type { MediaType } from 'lib/types/media-types.js'; import css from './media.css'; import MultimediaModal from './multimedia-modal.react.js'; import Button from '../components/button.react.js'; import { type PendingMultimediaUpload } from '../input/input-state.js'; type BaseProps = { +uri: string, +type: MediaType, +pendingUpload?: ?PendingMultimediaUpload, +remove?: (uploadID: string) => void, +multimediaCSSClass: string, +multimediaImageCSSClass: string, }; type Props = { ...BaseProps, +pushModal: PushModal, }; class Multimedia extends React.PureComponent { componentDidUpdate(prevProps: Props) { const { uri, pendingUpload } = this.props; if (uri === prevProps.uri) { return; } if ( (!pendingUpload || pendingUpload.uriIsReal) && (!prevProps.pendingUpload || !prevProps.pendingUpload.uriIsReal) ) { URL.revokeObjectURL(prevProps.uri); } } render(): React.Node { let progressIndicator, errorIndicator, removeButton; const { pendingUpload, remove, type, uri, multimediaImageCSSClass, multimediaCSSClass, } = this.props; if (pendingUpload) { const { progressPercent, failed } = pendingUpload; if (progressPercent !== 0 && progressPercent !== 1) { const outOfHundred = Math.floor(progressPercent * 100); const text = `${outOfHundred}%`; progressIndicator = ( ); } if (failed) { errorIndicator = ( ); } if (remove) { removeButton = ( ); } } const imageContainerClasses = [ css.multimediaImage, multimediaImageCSSClass, ]; imageContainerClasses.push(css.clickable); let mediaNode; if (type === 'photo') { mediaNode = ( ); } else { mediaNode = (
); } const containerClasses = [css.multimedia, multimediaCSSClass]; return ( {mediaNode} {progressIndicator} {errorIndicator} ); } remove: (event: SyntheticEvent) => void = event => { event.stopPropagation(); const { remove, pendingUpload } = this.props; invariant( remove && pendingUpload, 'Multimedia cannot be removed as either remove or pendingUpload ' + 'are unspecified', ); remove(pendingUpload.localID); }; onClick: () => void = () => { - const { pushModal, uri } = this.props; - pushModal(); + const { pushModal, type, uri } = this.props; + pushModal(); }; } function ConnectedMultimediaContainer(props: BaseProps): React.Node { const modalContext = useModalContext(); return ; } export default ConnectedMultimediaContainer; diff --git a/web/modals/threads/gallery/thread-settings-media-gallery.react.js b/web/modals/threads/gallery/thread-settings-media-gallery.react.js index ea96517e6..17ced2938 100644 --- a/web/modals/threads/gallery/thread-settings-media-gallery.react.js +++ b/web/modals/threads/gallery/thread-settings-media-gallery.react.js @@ -1,115 +1,130 @@ // @flow import * as React from 'react'; import { fetchThreadMedia } from 'lib/actions/thread-actions.js'; +import { useModalContext } from 'lib/components/modal-provider.react.js'; +import type { Media } from 'lib/types/media-types.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { useServerCall } from 'lib/utils/action-utils.js'; import css from './thread-settings-media-gallery.css'; import Tabs from '../../../components/tabs.react.js'; +import MultimediaModal from '../../../media/multimedia-modal.react.js'; import Modal from '../../modal.react.js'; type MediaGalleryTab = 'All' | 'Images' | 'Videos'; type ThreadSettingsMediaGalleryModalProps = { +onClose: () => void, +parentThreadInfo: ThreadInfo, +limit: number, +activeTab: MediaGalleryTab, }; function ThreadSettingsMediaGalleryModal( props: ThreadSettingsMediaGalleryModalProps, ): React.Node { + const { pushModal } = useModalContext(); const { onClose, parentThreadInfo, limit, activeTab } = props; const { id: threadID } = parentThreadInfo; const modalName = 'Media'; const callFetchThreadMedia = useServerCall(fetchThreadMedia); const [mediaInfos, setMediaInfos] = React.useState([]); const [tab, setTab] = React.useState(activeTab); React.useEffect(() => { const fetchData = async () => { const result = await callFetchThreadMedia({ threadID, limit, offset: 0, }); setMediaInfos(result.media); }; fetchData(); }, [callFetchThreadMedia, threadID, limit]); + const onClick = React.useCallback( + (media: Media) => { + pushModal(); + }, + [pushModal], + ); + const filteredMediaInfos = React.useMemo(() => { if (tab === 'Images') { return mediaInfos.filter(mediaInfo => mediaInfo.type === 'photo'); } else if (tab === 'Videos') { return mediaInfos.filter(mediaInfo => mediaInfo.type === 'video'); } return mediaInfos; }, [tab, mediaInfos]); const mediaCoverPhotos = React.useMemo( () => filteredMediaInfos.map(media => media.thumbnailURI || media.uri), [filteredMediaInfos], ); const mediaGalleryItems = React.useMemo( () => filteredMediaInfos.map((media, i) => ( -
+
onClick(media)} + className={css.mediaContainer} + >
)), - [filteredMediaInfos, mediaCoverPhotos], + [filteredMediaInfos, onClick, mediaCoverPhotos], ); const handleScroll = React.useCallback( async event => { const container = event.target; // Load more data when the user is within 1000 pixels of the end const buffer = 1000; if ( container.scrollHeight - container.scrollTop > container.clientHeight + buffer ) { return; } const result = await callFetchThreadMedia({ threadID, limit, offset: mediaInfos.length, }); setMediaInfos([...mediaInfos, ...result.media]); }, [callFetchThreadMedia, threadID, limit, mediaInfos], ); return (
{mediaGalleryItems}
{mediaGalleryItems}
{mediaGalleryItems}
); } export default ThreadSettingsMediaGalleryModal;